docs: the widened Qt consumer surface - #84
Merged
Conversation
…ames
A universal module's `getMethods()` comes from the cdylib backend's
`lidlInterfaceJson()` (logos-plugin-qt's glue forwards
`logos_module_get_methods` verbatim), and that now answers in the LIDL
contract spelling. `lm methods`, `lm events` and `logoscore module-info`
print those strings straight through, so every listing in Part 1 changed:
qlonglong add(qlonglong a, qlonglong b) -> int add(int a, int b)
QString libVersion() -> tstr libVersion()
void versionReady(QString version) -> void versionReady(tstr version)
Six `expect_contains` in tutorial-wrapping-c-library.test.yaml were pinned
to the Qt spellings and now fail.
HOW THIS WAS ALMOST MISSED, because the trap will recur. The hand-pinned
`outputs/tutorial-wrapping-c-library.md` already showed `int add(int a, int b)`
and `add(int,int)` — a stale snapshot from an earlier era that happened to
read as "already LIDL, nothing to do". CI runs the ASSERTIONS in
`tests/*.test.yaml`; it never diffs the outputs tree. Clearing a file by
reading `outputs/` proves nothing.
Every replacement string is derived mechanically rather than by hand: the
tutorial's own `src/calc_module_impl.h` + `metadata.json` were run through
`logos-cpp-generator --from-header --backend cdylib`, the emitted
`lidlInterfaceJson()` was parsed back into JSON, and that JSON was rendered
through logos-module's own printer (`cmd/main.cpp`) and logoscore's
(`src/client/output.cpp`). The displayed blocks in BOTH trees now compare
byte-identical to that render.
Two accuracy fixes fall out of doing that, both pre-existing drift in the
blocks being rewritten:
* the derived identity methods `name()` / `version()` DO appear in every
listing (nothing filters `derived` on the read side) and were missing
from the shown output;
* the `module-info` block said `libVersion() -> QString` and
`versionReady(version: QString)`.
The C++-type table gains a column. "On the wire (Qt)" conflated two
different questions; it is now "LIDL contract type" — what the module
publishes, what Step 5 prints, what a Rust or Nim binding sees — and "A Qt
consumer sees", which is only the C++/Qt caller's spelling.
Also here, same cause:
* tutorial-composing-modules and tutorial-interface-dependencies had the
same "shows up as QString ... the wire types the generated glue exposes"
prose. Their assertions are name-only, so they did not fail — but they
described the listing wrongly. `LogosMap` publishes as `{tstr: any}`,
verified by generating calc_aggregator's glue.
* logos-developer-guide.md's `lm methods --json` example was a
handwritten-Qt listing (`initLogos(LogosAPI*)`) presented as the general
case. It now shows both publishers and says which is which: a universal
module publishes its contract, a handwritten Qt plugin publishes what its
QMetaObject says.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s plugin `logos-cpp-generator <plugin> [--module-only]` is documented here as a way to generate a module's consumer wrapper, and the examples pass only the plugin. That is now a trap for any module built with `interface: "universal"` or `"cdylib"`: its published `getMethods()` answers in the LIDL contract vocabulary — the same change d963871 pinned in the `lm` listings — while the wrapper emitter reads Qt type names and falls back to QVariant / LogosMap for anything else. The wrapper would compile and have lost every type. The generator now takes the METHODS from the `.lidl` contract named by `--events-from` (the flag keeps its name; the file always was the whole contract), and REFUSES a LIDL-spelled listing when no contract was given rather than emitting the untyped wrapper. Nix builds already pass the flag — buildHeaders.nix finds `<module>/share/logos/<name>.lidl` — so only hand-run invocations, which is what this section documents, had to change. Both examples gain the flag, a second example shows the handcrafted-Qt case that legitimately omits it, and the synopsis in the CLI reference lists it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📊 Tutorial execution reportRendered tutorial alongside the commands actually run and their output (updated each run, commit Pages can take a minute to update after the run finishes. |
Khushboo-dev-cpp
pushed a commit
that referenced
this pull request
Aug 27, 2026
* test(doctests): `lm` publishes the LIDL contract vocabulary, not Qt names
A universal module's `getMethods()` comes from the cdylib backend's
`lidlInterfaceJson()` (logos-plugin-qt's glue forwards
`logos_module_get_methods` verbatim), and that now answers in the LIDL
contract spelling. `lm methods`, `lm events` and `logoscore module-info`
print those strings straight through, so every listing in Part 1 changed:
qlonglong add(qlonglong a, qlonglong b) -> int add(int a, int b)
QString libVersion() -> tstr libVersion()
void versionReady(QString version) -> void versionReady(tstr version)
Six `expect_contains` in tutorial-wrapping-c-library.test.yaml were pinned
to the Qt spellings and now fail.
HOW THIS WAS ALMOST MISSED, because the trap will recur. The hand-pinned
`outputs/tutorial-wrapping-c-library.md` already showed `int add(int a, int b)`
and `add(int,int)` — a stale snapshot from an earlier era that happened to
read as "already LIDL, nothing to do". CI runs the ASSERTIONS in
`tests/*.test.yaml`; it never diffs the outputs tree. Clearing a file by
reading `outputs/` proves nothing.
Every replacement string is derived mechanically rather than by hand: the
tutorial's own `src/calc_module_impl.h` + `metadata.json` were run through
`logos-cpp-generator --from-header --backend cdylib`, the emitted
`lidlInterfaceJson()` was parsed back into JSON, and that JSON was rendered
through logos-module's own printer (`cmd/main.cpp`) and logoscore's
(`src/client/output.cpp`). The displayed blocks in BOTH trees now compare
byte-identical to that render.
Two accuracy fixes fall out of doing that, both pre-existing drift in the
blocks being rewritten:
* the derived identity methods `name()` / `version()` DO appear in every
listing (nothing filters `derived` on the read side) and were missing
from the shown output;
* the `module-info` block said `libVersion() -> QString` and
`versionReady(version: QString)`.
The C++-type table gains a column. "On the wire (Qt)" conflated two
different questions; it is now "LIDL contract type" — what the module
publishes, what Step 5 prints, what a Rust or Nim binding sees — and "A Qt
consumer sees", which is only the C++/Qt caller's spelling.
Also here, same cause:
* tutorial-composing-modules and tutorial-interface-dependencies had the
same "shows up as QString ... the wire types the generated glue exposes"
prose. Their assertions are name-only, so they did not fail — but they
described the listing wrongly. `LogosMap` publishes as `{tstr: any}`,
verified by generating calc_aggregator's glue.
* logos-developer-guide.md's `lm methods --json` example was a
handwritten-Qt listing (`initLogos(LogosAPI*)`) presented as the general
case. It now shows both publishers and says which is which: a universal
module publishes its contract, a handwritten Qt plugin publishes what its
QMetaObject says.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(guide): the plugin path needs the module's contract, not just its plugin
`logos-cpp-generator <plugin> [--module-only]` is documented here as a way to
generate a module's consumer wrapper, and the examples pass only the plugin.
That is now a trap for any module built with `interface: "universal"` or
`"cdylib"`: its published `getMethods()` answers in the LIDL contract
vocabulary — the same change d963871 pinned in the `lm` listings — while the
wrapper emitter reads Qt type names and falls back to QVariant / LogosMap for
anything else. The wrapper would compile and have lost every type.
The generator now takes the METHODS from the `.lidl` contract named by
`--events-from` (the flag keeps its name; the file always was the whole
contract), and REFUSES a LIDL-spelled listing when no contract was given rather
than emitting the untyped wrapper. Nix builds already pass the flag —
buildHeaders.nix finds `<module>/share/logos/<name>.lidl` — so only hand-run
invocations, which is what this section documents, had to change.
Both examples gain the flag, a second example shows the handcrafted-Qt case
that legitimately omits it, and the synopsis in the CLI reference lists it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stops the Qt consumer surface losing type information the std C++ consumer keeps.
The gap
Most of the Qt mapping was already lossless —
bstr→QByteArray,uint→qulonglong,[Record]→QList<Record>. Three slots were not:?TQVariant— "the one mapping in this table that LOSES the value type"std::optional<qtOf(T)>[T]non-tstr/recordQVariantListQList<qtOf(T)>{tstr:V}non-recordQVariantMapQMap<QString, qtOf(V)>anyQVariantqtOfrecurses and bottoms out atany, so[any]staysQVariantList.The blocking objection turned out to be false. The
Optionalbranch warned that the type name feeds a Qt metatype andstd::optional<QString>isn't one. But no C++ generator emitsQ_OBJECTorQ_DECLARE_METATYPEfor a consumer wrapper — the twoQ_DECLARE_METATYPEhits in cpp-generator are comments explaining why they don't. Nothing the table emits reaches moc, and the introspection strings come from a separate function.getMethods publishes the contract vocabulary
returnType,parameters[].typeandsignatureare now?tstr,[Point],{tstr: uint}— the spellinglidl::serializewrites, pinned by a test that round-trips through the serializer rather than hardcoding a second spelling.This had a machine reader, and the first attempt broke it.
logos-cpp-generator --module-only <plugin>builds every lp dependency wrapper from published metadata, and fell back toQVariantfor unrecognised names — sotstr→LogosMap.checks.unit-tests-new-apiwent PASS→FAIL, bisected to exactly that commit. Fixed by having the header generator take methods from the.lidlsidecar it already receives, so it no longer depends on published-metadata vocabulary at all. Verified:EXIT=1→EXIT=0, 32 passed.The trap that would have corrupted data silently
A widened type name must never reach
toWire/fromWire<T>unchanged:qvariantToNlohmannmatches a closed userType set, soQList<qulonglong>serialises tonull, andqvariant_cast<QList<qulonglong>>on aQVariantListreturns empty — no diagnostic either way. Encoding is generator-emitted element loops.The root cause of the two non-compiling nested shapes was duplication:
QList<Record>/QMap<QString,Record>had hand-written loops beside the generic ones, and the generic ones take their source as a lambda parameter while the record ones inlined it. Invisible at depth 1, fatal at depth 2. The record branches are deleted, not patched — the generic loops emit byte-identical text.Two real defects fixed on the way
["x", 5]read as[uint]reached a Qt author as[0, 5], silently. Now rejected, with the codec's own message on the error channel._bytescollision (conformance xfail M3):{"_bytes": "hello"}in a typed map slot arrived empty.Element rejection now reaches
*errat every depth, including inside record fields. Two paths stay lenient deliberately and are pinned by tests that assert the leniency: a bare scalar slot (the shipped contract of both consumer surfaces) and a missing record key.Verification
Round-tripped through real generated code with real values:
[uint]at2^64-1,[bstr]with embedded NULs,?Tpresent/absent/null,[[uint]],{tstr:[uint]},[?tstr](holes keep position). NineC1(C2(L))pairs × two leaf kinds, enumerated not sampled, with compilation as the assertion. 271→282 tests, 0 failures.Negative controls measured, not guessed:
for (auto __i = __i1→0,const nlohmann::json& __src20→0.Two independent adversarial audits returned BROKEN on earlier drafts; every defect they found is closed and re-audited.
Landing notes
ae3ffe0fat the qt-sdk and plugin-qt nodes, or qt-generator fails to compile ('lidl/identity.hpp' file not found).--api-style lpheader build for a Rust dependency into a hard failure.🤖 Generated with Claude Code